package hw01;

import java.sql.Timestamp;
import java.util.Calendar;
import java.util.HashSet;
import java.util.Iterator;
import java.util.Set;
import java.util.regex.Pattern;

/**
 * <p>Theis class provides an {@link Iterator} for generating the {@link Timestamp}'s that 
 * match a pattern p.  This class can be used in the following manner to get all 
 * {@link Timestamp} t that match a given pattern.
 * <pre>
 * {@code
 * for(Timestamp t : new Cron("0 0 1 * * *")) {
 * 		System.out.println(t);
 * }
 * }
 * </pre>
 * </p>
 * 
 * <p>[A timestamp matches p] when the minute, hour, and month of year fields match the current time, 
 * and when at least one of the two day fields (day of month, or day of week) match the current time.  
 * The time and date fields are:
 * 
 * <table>
 * <tr><th>field</th><th>allowed values</th></tr>
 * <tr><td>minute</td><td>0-59</td></tr>
 * <tr><td>hour</td><td>0-23</td></tr>
 * <tr><td>day of month</td><td>1-31</td></tr>
 * <tr><td>month</td><td>1-12 or names</td></tr>
 * <tr><td>day of week</td><td>0-7 (0 and 7 mean Sun.)</td></tr>
 * <tr><td>[year]</td><td>[integer year of common era]</td></tr>
 * </table>
 * </p>
 * 
 * <p>A field may be an asterisk (*), which always stands for "first-last."</p>
 * 
 * <p>Ranges of numbers are allowed.  Ranges are two numbers separated with a hyphen.  The specified 
 * range is inclusive.  For example, 8-11 for an "hours" entry specifies [a reservation] at hours 
 * 8, 9, 10 and 11.  [Similarly, 2009-2011 specifies a reservation for years 2009, 2010 and 2011 C.E.  
 * We will assume a temporal granularity of one minute.  Thus the time-pattern * * 1 1 * 2009 
 * specifies the time points marking each minute of each hour of January 1st, 2009 (a total of 60x60 
 * points in time).</p>
 * 
 * <p>List are allowed.  A list is a set of numbers (or ranges) separated by commas.  
 * Examples: "1,2,5,9"; "0-4,8-12."</p>
 * 
 * <p>Step values can be used in conjunction with ranges.  Following a range with "/<number>" 
 * specifies skips of the number's value through the range.  For example, "0-23/2" can be 
 * used in the hours field to specify command execution every other hour (the alternative in 
 * the V7 standard is "0,2,4,6,8,10,12,14,16,18,20,22).  Steps are also permitted after an 
 * asterisk, so if you want to say "every two hours," just use "* /2".</p>
 * 
 * <p>Names can also be used for the "month" and "day of week" fields.  Use the first three 
 * letters of the particular day or month (case doesn't matter).  Ranges or lists of names 
 * are not allowed.</p>
 * 
 * <p>A single invocation of {@link Questions#makeReservations(String, int, String, int, String)} 
 * creates, in general, several entries in the Reservations table.  We will refer to this 
 * group of reservations as a <em>reservation group</em>. The time-length argument specifies 
 * following invocation results in 24 reservations for January 15th, 2011 for the first 10 
 * minutes of each hour of that day:
 * <code>makeReservation(0 * 15 * 2011, 10, Neville, 210, Alice)</code>
 * </p>
 * 
 * <p><b>Time-limit Assumption.</b>  You may ignore reservations beyond 2012-12-31 23:59:59.  
 * For patterns that specify reservations both before and after this timestamp, only the 
 * reservations after the date may be ignored; the earlier ones must be managed properly.  
 * This assumption may simplify your implementation of invocations such as the following:
 * <code>makeReservation(0 * 15 1 * *, 10, Neville, 210, Alice)</code></p>
 * 
 * @author Joshua Belanger
 * @version 1.0
 * @see Iterable
 */
public class Cron implements Iterable<Timestamp> {
	
//	public static void main(String[] args) {
//		for(Timestamp t : new Cron("0 0 1 1 FRI *")) {
//			System.out.println(t);
//		}
//	}
	
	protected String timePattern;
	protected int timeLength;
	protected Calendar start;
	protected Calendar end;
	protected Pattern domPattern;
	protected Pattern dowPattern;
	
	protected enum CronField {
		MINUTE,
		HOUR,
		DAY_OF_MONTH,
		MONTH,
		DAY_OF_WEEK,
		YEAR
	}
	
	/**
	 * Initializes object with a timeLength of 1
	 * 
	 * @param timePattern Pattern to match
	 * @see Cron
	 */
	public Cron(String timePattern) {
		initialize(timePattern, 1);
	}

	/**
	 * Initializes object
	 * 
	 * @param timePattern Pattern to match
	 * @param timeLength length of each match
	 * @see Cron
	 */
	public Cron(String timePattern, int timeLength) {
		initialize(timePattern, timeLength);
	}
	
	/**
	 * Does the work for the constructors
	 * 
	 * @param timePattern Pattern to match
	 * @param timeLength length of each match in minutes
	 */
	private void initialize(String timePattern, int timeLength) {
		this.timePattern = timePattern;
		this.timeLength = timeLength;
		
		this.start = Calendar.getInstance();
		this.start.set(Calendar.MILLISECOND, 0);
		this.start.set(Calendar.SECOND, 0);
		
		this.end = Calendar.getInstance();
		this.end.setTimeInMillis(Timestamp.valueOf("2012-12-31 23:59:59").getTime());
		
		String[] cronSplit = timePattern.split(" ", 6);
		String minute = getRegex(cronSplit[0], CronField.MINUTE);
		String hour = getRegex(cronSplit[1], CronField.HOUR);
		String dayOfMonth = getRegex(cronSplit[2], CronField.DAY_OF_MONTH);
		String month = getRegex(cronSplit[3], CronField.MONTH);
		String year = getRegex(cronSplit[5], CronField.YEAR);
		
		String domRegex = year + "-" + month + "-" + dayOfMonth + " " + hour + ":" + minute + ":.*";
		this.domPattern = Pattern.compile(domRegex);
		
		String dowRegex;
		if("*".equals(cronSplit[4])) {
			dowRegex = "";
		} else {
			String dayOfWeek = getRegex(cronSplit[4], CronField.DAY_OF_WEEK);
			dowRegex = dayOfWeek + ":" + year + "-" + month + "-\\d\\d " + hour + ":" + minute + ":.*";
		}
		this.dowPattern = Pattern.compile(dowRegex);
		
		
//		System.out.println("CRON:" + timePattern);
//		System.out.println("DOM REGEX:" + domRegex);
//		System.out.println("DOW REGEX:" + dowRegex);
//		System.out.println("START:" + new Timestamp(start.getTimeInMillis()));
//		System.out.println("END:" + new Timestamp(end.getTimeInMillis()));
	}
	
	/**
	 * @param cronElement Element of cron string
	 * @param type The type of the cronElement
	 * @return Regular expression to match cronElement
	 */
	private String getRegex(String cronElement, CronField type) {
		Set<String> valid = new HashSet<String>();
		
		// /<number> === stepSplit[0] \ step
		String[] stepSplit = cronElement.split("/", 2);
		int step = stepSplit.length == 2 ? parseField(stepSplit[1], type) : 1;

		// 1,2,3, ..., n === listSplit[0], listSplit[1], listSplit[2], ..., listSplit[n]
		String[] listSplit = stepSplit[0].split(",");
		for(String listElement : listSplit) {

			// * === first-last === listElement
			if("*".equals(listElement)) {
				listElement = starToRange(type);
			}

			// a-b === j-n === j, j+1, j+2, ..., j+(b-a)
			String[] dashSplit = listElement.split("-", 2);
			for(
				int j = parseField(dashSplit[0], type),
				n = dashSplit.length == 2 ? parseField(dashSplit[1], type) : j;
				j <= n;
				j++
			) {
				
				if(j % step == 0) {
					valid.add(String.format("%02d", j));
				}
				
			}
		}
		
		// build regex so it becomes (valid[0]|valid[1]|...|valid[n])
		String regex = "(";
		Iterator<String> iter = valid.iterator();
	    while (iter.hasNext()) {
	        regex += iter.next();
	        if (iter.hasNext()) {
	            regex += "|";
	        }
	    }
		regex += ")";
		
		return regex;
	}
	
	/**
	 * Parses cron input into corresponding int form
	 * 
	 * @param field Cron input to parse
	 * @param type Type of cron field is
	 * @return Parsed input in int form
	 */
	private int parseField(String field, CronField type) {
		switch (type) {
		case DAY_OF_WEEK:
			if(field.equalsIgnoreCase("SUN")) {
				return 0;
			} else if(field.equalsIgnoreCase("MON")) {
				return 1;
			} else if(field.equalsIgnoreCase("TUE")) {
				return 2;
			} else if(field.equalsIgnoreCase("WED")) {
				return 3;
			} else if(field.equalsIgnoreCase("THU")) {
				return 4;
			} else if(field.equalsIgnoreCase("FRI")) {
				return 5;
			} else if(field.equalsIgnoreCase("SAT")) {
				return 6;
			}
		case MONTH:
			if(field.equalsIgnoreCase("JAN")) {
				return 1;
			} else if(field.equalsIgnoreCase("FEB")) {
				return 2;
			} else if(field.equalsIgnoreCase("MAR")) {
				return 3;
			} else if(field.equalsIgnoreCase("APR")) {
				return 4;
			} else if(field.equalsIgnoreCase("MAY")) {
				return 5;
			} else if(field.equalsIgnoreCase("JUN")) {
				return 6;
			} else if(field.equalsIgnoreCase("JUL")) {
				return 7;
			} else if(field.equalsIgnoreCase("AUG")) {
				return 8;
			} else if(field.equalsIgnoreCase("SEP")) {
				return 9;
			} else if(field.equalsIgnoreCase("OCT")) {
				return 10;
			} else if(field.equalsIgnoreCase("NOV")) {
				return 11;
			} else if(field.equalsIgnoreCase("DEC")) {
				return 12;
			}
		default:
			return Integer.parseInt(field);
		}
	}
	
	/**
	 * Converts * cron input into proper regex
	 * 
	 * @param type Cron type
	 * @return regex for * cron
	 */
	private String starToRange(CronField type) {
		switch (type) {
		case MINUTE:
			return "00-59";
		case HOUR:
			return "00-23";
		case DAY_OF_MONTH:
			return "01-31";
		case MONTH:
			return "01-12";
		case DAY_OF_WEEK:
			return "00-07";
		case YEAR:
			return "2011-2012";
		default:
			return "";
		}
	}
	
	@Override
	public Iterator<Timestamp> iterator() {
		return new CronIterator(this);
	}
}

class CronIterator implements Iterator<Timestamp> {

	public Cron cron;
	public Calendar current;
	public int increment;

	
	public CronIterator(Cron cron) {
		this.cron = cron;
		this.current = Calendar.getInstance();
		this.current.setTime(cron.start.getTime());
		this.increment = Calendar.MINUTE;
	}
	
	@Override
	public boolean hasNext() {
		while(current.before(cron.end)) {
			String domFormat = new Timestamp(current.getTimeInMillis()).toString();
			String dowFormat = String.format("%02d", current.get(Calendar.DAY_OF_WEEK) - 1) + ":" + domFormat;
			if(cron.domPattern.matcher(domFormat).matches() || cron.dowPattern.matcher(dowFormat).matches()) {
				return true;
			}
			current.add(increment, 1);
		}
		return false;
	}

	@Override
	public Timestamp next() {
		Timestamp next = new Timestamp(current.getTimeInMillis());
		current.add(increment, cron.timeLength);
		return next;
	}

	@Override
	public void remove() {}
	
}